/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;

Cu.importGlobalProperties(["Blob"]);

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
  "resource://gre/modules/WebChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
  "resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
  "resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
  "resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
  "resource://gre/modules/PageThumbs.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
  "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
  "resource://gre/modules/FxAccounts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BackgroundPageThumbs",
  "resource://gre/modules/BackgroundPageThumbs.jsm");

XPCOMUtils.defineLazyGetter(this, "gMM", function() {
  return Cc["@mozilla.org/globalmessagemanager;1"].
    getService(Ci.nsIMessageListenerManager);
});

XPCOMUtils.defineLazyModuleGetter(this, "delayedSuggestBaidu",
  "resource://ntab/mozCNUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Frequent",
  "resource://ntab/mozCNUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Homepage",
  "resource://ntab/mozCNUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Session",
  "resource://ntab/mozCNUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "getPref",
  "resource://ntab/mozCNUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NTabDB",
  "resource://ntab/NTabDB.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NTabSync",
  "resource://ntab/NTabSync.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PartnerBookmarks",
  "resource://ntab/PartnerBookmarks.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "QuickDialData",
  "resource://ntab/QuickDialData.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Tracking",
  "resource://ntab/Tracking.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "Promo",
  "resource://cehp-promo/Promo.jsm");

let searchEngines = {
  expected: /^https?:\/\/www\.baidu\.com\/baidu\?wd=TEST&tn=monline(?:_|_4_)dg(?:&ie=utf-8)?$/,

  reportUnexpected: function(aKey, aAction, aEngine, aIncludeURL) {
    let url = "NA";
    try {
      url = aEngine.getSubmission("TEST").uri.asciiSpec;
    } catch(e) {}

    let isExpected = this.expected.test(url);
    let href = "";
    if (!isExpected && !!aIncludeURL) {
      href = url;
    }

    Tracking.track({
      type: "searchplugins",
      action: aAction,
      sid: aKey,
      fid: isExpected,
      href: href
    });
  },

  patchBrowserSearch: function(aWindow) {
    // Since Fx 44
    if (aWindow.gURLBar && aWindow.gURLBar._parseAndRecordSearchEngineAction) {
      try {
        Services.obs.removeObserver(this, "keyword-search");
      } catch(ex) {};
    }

    let BrowserSearch = aWindow.BrowserSearch;
    if (!BrowserSearch || !BrowserSearch.recordSearchInHealthReport) {
      return;
    }

    let origRSIHR = BrowserSearch.recordSearchInHealthReport;
    let self = this;
    BrowserSearch.recordSearchInHealthReport = function(aEngine, aSource) {
      origRSIHR.apply(BrowserSearch, [].slice.call(arguments));

      self.trackUsage(aEngine, aSource);
    };
  },

  trackUsage: function(aEngine, aSource) {
    try {
      if (!(aEngine instanceof Ci.nsISearchEngine) ||
          typeof(aSource) != "string") {
        return;
      }

      let key = {
        "\u767e\u5ea6": "baidu"
      }[aEngine.name] || "other";

      this.reportUnexpected(key, aSource, aEngine, false);
    } catch(e) {};
  },

  removeLegacyEngines: function() {
    [{
      legacy: Services.search.getEngineByName("\u5353\u8d8a\u4e9a\u9a6c\u900a"),
      update: Services.search.getEngineByName("\u4e9a\u9a6c\u900a")
    }, {
      legacy: Services.search.getEngineByName("\u6dd8\u5b9d\u8d2d\u7269"),
      update: Services.search.getEngineByName("\u7231\u6dd8\u5b9d")
    }, {
      legacy: Services.search.getEngineByName("\u7231\u6dd8\u5b9d\u8d2d\u7269"),
      update: Services.search.getEngineByName("\u7231\u6dd8\u5b9d")
    }].forEach(function(aEngines) {
      if ((aEngines.legacy && !aEngines.legacy.hidden) &&
          (aEngines.update && !aEngines.update.hidden)) {
        if (Services.search.currentEngine == aEngines.legacy) {
          Services.search.currentEngine = aEngines.update;
        }
        Services.search.removeEngine(aEngines.legacy);
      }
    });
  },

  init: function() {
    let self = this;

    Services.obs.addObserver(this, "keyword-search", false);
    Services.search.init(function() {
      let current = Services.search.currentEngine,
          baidu = Services.search.getEngineByName("\u767e\u5ea6");
      self.reportUnexpected("current", "detect", current, true);
      self.reportUnexpected("baidu", "detect", baidu, true);
      self.removeLegacyEngines();
    });
  },

  observe: function(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "keyword-search":
        this.trackUsage(aSubject, "urlbar");
        break;
    }
  }
};

let fxAccountsProxy = {
  messageName: "mozCNUtils:FxAccounts",
  mutationConfig: {
    attributes: true,
    attributeFilter: [
      "disabled",
      "failed",
      "fxastatus",
      "hidden",
      "label",
      "signedin",
      "status",
      "tooltiptext"
    ]
  },
  getFxaOrSync: function(aWindow) {
    if (!aWindow) {
      return;
    }
    // Since Fx 55, see https://bugzil.la/1353571 & https://bugzil.la/1354084
    if (aWindow.gSync) {
      return {
        container: aWindow.gSync.appMenuContainer,
        label: aWindow.gSync.appMenuLabel,
        status: aWindow.gSync.appMenuStatus
      };
    }
    if (aWindow.gFxAccounts) {
      return {
        button: aWindow.gFxAccounts.button,
        container: aWindow.gFxAccounts.panelUIFooter,
        label: aWindow.gFxAccounts.panelUILabel,
        status: aWindow.gFxAccounts.panelUIStatus
      }
    }
  },
  generateKVs: function(fxaOrSync) {
    let kvs = {};

    // Before Fx 41
    if (fxaOrSync.button) {
      this.mutationConfig.attributeFilter.forEach(function(aKey) {
        if (fxaOrSync.button.hasAttribute(aKey)) {
          kvs[aKey] = fxaOrSync.button.getAttribute(aKey);
        }
      });
    } else if (fxaOrSync.container &&
               fxaOrSync.label &&
               fxaOrSync.status) {
      ["disabled", "fxastatus"].forEach(function(aKey) {
        if (fxaOrSync.container.hasAttribute(aKey)) {
          kvs[aKey] = fxaOrSync.container.getAttribute(aKey);
        }
      });
      ["label"].forEach(function(aKey) {
        if (fxaOrSync.label.hasAttribute(aKey)) {
          kvs[aKey] = fxaOrSync.label.getAttribute(aKey);
        }
      });
      ["tooltiptext"].forEach(function(aKey) {
        if (fxaOrSync.status.hasAttribute(aKey)) {
          kvs[aKey] = fxaOrSync.status.getAttribute(aKey);
        }
      });
    }

    return kvs;
  },
  maybeRegisterMutationObserver: function(aWindow) {
    let fxaOrSync = this.getFxaOrSync(aWindow);
    let windowMM = aWindow.messageManager;
    if (!fxaOrSync || !windowMM) {
      return;
    }

    let self = this;
    let config = this.mutationConfig;
    let observer = new aWindow.MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        if (mutation.type != "attributes" ||
            config.attributeFilter.indexOf(mutation.attributeName) < 0) {
          return;
        }

        let kvs = self.generateKVs(fxaOrSync);
        windowMM.broadcastAsyncMessage(self.messageName, "mutation", kvs);
      });
    });

    // Before Fx 41
    if (fxaOrSync.button) {
      observer.observe(fxaOrSync.button, config);
    } else {
      observer.observe(fxaOrSync.container, config);
      observer.observe(fxaOrSync.label, config);
      observer.observe(fxaOrSync.status, config);
    }
  },
  maybeInitContentButton: function(aBrowser) {
    let fxaOrSync = this.getFxaOrSync(aBrowser.ownerGlobal);
    let browserMM = aBrowser.messageManager;
    if (!fxaOrSync || !browserMM) {
      return;
    }

    let kvs = this.generateKVs(fxaOrSync);
    browserMM.sendAsyncMessage(this.messageName, "init", kvs);
  },
  init: function() {
    gMM.addMessageListener(this.messageName, this);
  },
  // nsIMessageListener
  receiveMessage: function(aMessage) {
    let browser = aMessage.target;
    if (aMessage.name !== this.messageName ||
        !browser.currentURI.equals(NTabDB.uri)) {
      return;
    }

    switch(aMessage.data) {
      case "init":
        this.maybeInitContentButton(browser);
        break;
      case "action":
        let fxaOrSync = (browser.ownerGlobal.gSync ||
                         browser.ownerGlobal.gFxAccounts);
        fxaOrSync.onMenuPanelCommand(aMessage.objects);
        Tracking.track({
          type: "ntabsync",
          action: "click",
          sid: "in-content"
        });
        break;
    }
  },
};

let appcacheTempFix = {
  attribute: "mozCNAppcacheTempFix",
  delay: 3e3,
  fixApplied: false,

  get appCacheService() {
    delete this.appCacheService;
    return this.appCacheService =
      Cc["@mozilla.org/network/application-cache-service;1"].
        getService(Ci.nsIApplicationCacheService);
  },

  clear: function(aHost) {
    let groups = this.appCacheService.getGroups();
    for (let i = 0; i < groups.length; i++) {
      let uri = Services.io.newURI(groups[i], null, null);
      if (uri.asciiHost == aHost) {
        let cache = this.appCacheService.getActiveCache(groups[i]);
        cache.discard();
      }
    }
    this.fixApplied = true;

    Tracking.track({
      type: "appcache",
      action: "clear",
      sid: "dummy"
    });
  },

  attach: function(aBrowser, aRequest) {
    this.remove(aBrowser, aRequest);
    if (this.fixApplied) {
      return;
    }

    let timeoutId = setTimeout((function() {
      if (aBrowser.mDestroyed !== false) {
        return;
      }

      this.clear(aRequest.URI.asciiHost);

      aRequest.cancel(Cr.NS_BINDING_ABORTED);
      aBrowser.webNavigation.loadURI(aRequest.URI.spec, null, null, null, null);
    }).bind(this), this.delay);
    aBrowser.setAttribute(this.attribute, timeoutId);
  },

  remove: function(aBrowser, aRequest) {
    if (aBrowser.hasAttribute(this.attribute)) {
      let timeoutId = aBrowser.getAttribute(this.attribute);
      aBrowser.removeAttribute(this.attribute);
      clearTimeout(parseInt(timeoutId, 10));
    }
  }
}

function mozCNUtils() {}

mozCNUtils.prototype = {
  classID: Components.ID("{828cb3e4-a050-4f95-8893-baa0b00da7d7}"),

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                         Ci.nsIMessageListener]),

  // nsIObserver
  observe: function(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "profile-after-change":
        Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
        Services.obs.addObserver(this, "http-on-examine-response", false);
        Services.obs.addObserver(this, "http-on-examine-cached-response", false);
        Services.obs.addObserver(this, "http-on-examine-merged-response", false);
        Services.obs.addObserver(this, "places-browser-init-complete", false);
        mozCNWebChannels.init();
        this.initFrameScripts();
        NTabDB.migrateNTabData();
        this.initMessageListener();
        delayedSuggestBaidu.init();
        Homepage.init();
        searchEngines.init();
        fxAccountsProxy.init();
        NTabSync.init();
        Promo.init();
        break;
      case "browser-delayed-startup-finished":
        this.initProgressListener(aSubject);
        searchEngines.patchBrowserSearch(aSubject);
        fxAccountsProxy.maybeRegisterMutationObserver(aSubject);
        break;
      case "http-on-examine-response":
      case "http-on-examine-cached-response":
      case "http-on-examine-merged-response":
        this.trackHTTPStatus(aSubject, aTopic);
        break;
      case "places-browser-init-complete":
        PartnerBookmarks.init();
        break;
    }
  },

  initFrameScripts: function() {
    [
      "chrome://ceerrorpage/content/ceerrorpage.js",
      "chrome://ntab/content/ntabContent.js"
    ].forEach(function(frameScript) {
      gMM.loadFrameScript(frameScript, true);
    });
  },

  trackHTTPStatus: function(aSubject, aTopic) {
    let channel = aSubject;
    channel.QueryInterface(Ci.nsIHttpChannel);

    if ([
      NTabDB.prePath,
      "http://i.g-fox.cn",
      "http://e.firefoxchina.cn",
      "http://i.firefoxchina.cn",
      "http://n.firefoxchina.cn"
    ].indexOf(channel.URI.prePath) == -1 ||
        !(channel.loadFlags & Ci.nsIChannel.LOAD_DOCUMENT_URI)) {
      return;
    }

    if ([200, 302, 304].indexOf(channel.responseStatus) == -1) {
      Tracking.track({
        type: "http-status",
        sid: channel.responseStatus,
        action: aTopic,
        href: channel.URI.spec,
        altBase: "http://robust.g-fox.cn/ntab.gif"
      });
    }
  },

  // nsIMessageListener
  receiveMessage: function(aMessage) {
    if (this.MESSAGES.indexOf(aMessage.name) < 0) {
      return;
    }

    let w = aMessage.target.ownerGlobal;

    switch (aMessage.name) {
      case "mozCNUtils:Tracking":
        Tracking.track(aMessage.data);
        break;
      case "mozCNUtils:WebChannel":
        switch ((aMessage.data && aMessage.data.type)) {
          case "isBaiduCurrentSearch":
            let topURI = aMessage.target.currentURI;
            aMessage.target.messageManager.sendAsyncMessage(aMessage.name, {
              type: aMessage.data.type,
              data: {
                exists: !!delayedSuggestBaidu.baidu,
                isCurrent: (delayedSuggestBaidu.baidu &&
                            (Services.search.currentEngine ===
                             delayedSuggestBaidu.baidu)),
                searchText: (delayedSuggestBaidu.isGoogleSearch(topURI) ?
                             delayedSuggestBaidu.extractKeyword(topURI) :
                             topURI.spec)
              }
            });
            break;
          case "isFxDefaultBrowser":
            // a response should be sent even shellService not available
            aMessage.target.messageManager.sendAsyncMessage(aMessage.name, {
              type: aMessage.data.type,
              data: (this.shellService &&
                     this.shellService.isDefaultBrowser(false, false))
            });
            break;
          case "setBaiduAsCurrentSearch":
            delayedSuggestBaidu.baidu.hidden = false;
            Services.search.currentEngine = delayedSuggestBaidu.baidu;

            Tracking.track({
              type: "delayedsuggestbaidu",
              action: "submit",
              sid: "switch"
            });
            break;
          case "setFxAsDefaultBrowser":
            if (!this.shellService) {
              break;
            }
            this.shellService.setDefaultBrowser(false, false);
            break;
        }
        break;
    }
  },

  get shellService() {
    let shellService;
    try {
      shellService = Cc['@mozilla.org/browser/shell-service;1'].
        getService(Ci.nsIShellService);
    } catch(e) {
      // shellService is not universally available, see https://bugzil.la/297841
    }
    delete this.shellService;
    return this.shellService = shellService;
  },

  MESSAGES: [
    "mozCNUtils:Tracking",
    "mozCNUtils:WebChannel"
  ],
  initMessageListener: function() {
    for (let msg of this.MESSAGES) {
      gMM.addMessageListener(msg, this);
    }
  },

  // TabsProgressListener variant of nsIWebProgressListener
  onStateChange: function(aBrowser, b, aRequest, aStateFlags, aStatus) {
    if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
      let isStart = aStateFlags & Ci.nsIWebProgressListener.STATE_START;
      let isStop = aStateFlags & Ci.nsIWebProgressListener.STATE_STOP;
      if (!isStart && !isStop) {
        return;
      }

      if (NTabDB.uri.equals(aRequest.URI)) {
        if (isStart) {
          appcacheTempFix.attach(aBrowser, aRequest);
        }
        if (isStop) {
          appcacheTempFix.remove(aBrowser, aRequest);
        }
      }

      if (delayedSuggestBaidu.isGoogleSearch(aRequest.URI)) {
        if (isStart) {
          delayedSuggestBaidu.attach(aBrowser, aRequest);
        }
        if (isStop) {
          delayedSuggestBaidu.remove(aBrowser, aRequest);
        }
      }
    }
  },

  onLocationChange: function(aBrowser, b, aRequest, aLocation, aFlags) {
    if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
      // before we can fix the OfflineCacheInstaller ?
      if (aLocation.equals(NTabDB.uri)) {
        aRequest.cancel(Cr.NS_BINDING_ABORTED);
        aBrowser.webNavigation.loadURI("about:blank", null, null, null, null);
      }
    }
  },

  initProgressListener: function(aSubject) {
    aSubject.gBrowser.addTabsProgressListener(this);
  }
};

let mozCNWebChannel = function(aChannelID, aURI, aListener) {
  this.channel = new WebChannel(aChannelID, aURI);
  this.channel.listen((this[aListener] || this.baseListener).bind(this));
};
mozCNWebChannel.prototype = {
  baseListener: function (a, aMessage, aSender) {
    let self = this;
    switch(aMessage.key) {
      case "frequent.query":
        Frequent.query(function(aEntries) {
          self.channel.send({
            id: aMessage.id,
            key: aMessage.key,
            data: aEntries
          }, aSender);
        }, aMessage.parameters.limit);
        break;
      case "frequent.remove":
        Frequent.remove([aMessage.parameters.url]);
        break;
      case "frequent.tophosts":
        Frequent.topHosts(function(aEntries) {
          self.channel.send({
            id: aMessage.id,
            key: aMessage.key,
            data: aEntries
          }, aSender);
        }, aMessage.parameters.hosts);
        break;
      case "last.query":
        Session.query(function(aEntries) {
          self.channel.send({
            id: aMessage.id,
            key: aMessage.key,
            data: aEntries
          }, aSender);
        }, aMessage.parameters.limit);
        break;
      case "last.remove":
        Session.remove([aMessage.parameters.url]);
        break;
      case "startup.channelid":
        this.channel.send({
          id: aMessage.id,
          key: aMessage.key,
          data: getPref("app.chinaedition.channel", "www.firefox.com.cn")
        }, aSender);
        break;
    }
  },
  offlintabListener: function (a, aMessage, aSender) {
    this.baseListener.apply(this, arguments);

    let self = this;
    switch(aMessage.key) {
      case "bookmark.query":
        let db = Cc["@mozilla.org/browser/nav-history-service;1"].
                   getService(Ci.nsINavHistoryService).
                   QueryInterface(Ci.nsPIPlacesDatabase).
                   DBConnection;
        let sql = ("SELECT b.title as title, p.url as url " +
                   "FROM moz_bookmarks b, moz_places p " +
                   "WHERE b.type = 1 AND b.fk = p.id AND p.hidden = 0");
        let statement = db.createAsyncStatement(sql);
        let links = [];
        db.executeAsync([statement], 1, {
          handleResult: function(aResultSet) {
            let row;

            while (row = aResultSet.getNextRow()) {
              links.push({
                title: row.getResultByName("title"),
                url: row.getResultByName("url")
              });
            }
          },
          handleError: function(aError) {
            self.channel.send({
              id: aMessage.id,
              key: aMessage.key,
              data: []
            }, aSender);
          },
          handleCompletion: function(aReason) {
            self.channel.send({
              id: aMessage.id,
              key: aMessage.key,
              data: links
            }, aSender);
          }
        });
        break;
      case "thumbs.getThumbnail":
        /**
         * use capture instead of captureIfMissing to force generate the
         * better looking version.
         */
        let url = aMessage.parameters.url;
        BackgroundPageThumbs.capture(url, {
          onDone: function() {
            let path = PageThumbs.getThumbnailPath(url);
            OS.File.read(path).then(function(aData) {
              let blob = new Blob([aData], {
                type: PageThumbs.contentType
              });
              self.channel.send({
                id: aMessage.id,
                key: aMessage.key,
                data: {
                  url: url,
                  blob: blob
                }
              }, aSender);
            }, function(aError) {
              self.channel.send({
                id: aMessage.id,
                key: aMessage.key,
                data: {
                  url: url
                }
              }, aSender);
            });
          }
        });
        break;
      case "variant.channel":
        this.channel.send({
          id: aMessage.id,
          key: aMessage.key,
          data: QuickDialData.variant
        }, aSender);
        break;
    }
  }
};


let mozCNWebChannels = {
  contentURL: "chrome://ntab/content/mozCNWebChannelContent.js",
  specs: {
    "http://e.firefoxchina.cn/": "",
    "http://i.firefoxchina.cn/": "",
    "http://n.firefoxchina.cn/": "",
    "http://newtab.firefoxchina.cn/": "",
    "http://offlintab.firefoxchina.cn/": "offlintabListener"
  },
  get channelID() {
    let prefKey = "webchannel.allowObject.urlWhitelist";
    delete this.channelID;
    return this.channelID = Services.prefs.getPrefType(prefKey) ?
                            "moz_cn_channel_v2" : "moz_cn_utils";
  },
  init: function () {
    let self = this;
    Object.keys(this.specs).forEach(function(aSpec) {
      let uri = Services.io.newURI(aSpec, null, null);
      new mozCNWebChannel(self.channelID, uri, self.specs[aSpec]);
    });
    gMM.loadFrameScript(this.contentURL, true);
  }
}

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([mozCNUtils]);
